package com.jelly.concurrency.syn;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Pair { // Not thread-safe
	
	private int x, y;

	Pair(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public Pair() {
		this(0, 0);
	}

	public int getX() {
		return x;
	}

	public int getY() {
		return y;
	}

	public void incrementX() {
		x++;
	}

	public void incrementY() {
		y++;
	}

	@Override
	public String toString() {
		return "x: " + x + ",y: " + y;
	}

	public class PairValuesNotEqualException extends RuntimeException {

		private static final long serialVersionUID = -6521275229471008406L;

		public PairValuesNotEqualException() {
			super("Pair values not equel: " + Pair.this);
		}
	}
	
	public void checkState(){
		if(x!=y){
			throw new PairValuesNotEqualException();
		}
	}
}
/**
 * Protect a Pair inside a thread-safe class
 * @author Jelly
 *
 */
 abstract class PairManager{
	 AtomicInteger checkCount = new AtomicInteger(0);
	 protected Pair p = new Pair();
	 private List<Pair> storage = Collections.synchronizedList(new ArrayList<Pair>());
	
	 public synchronized Pair getPair(){
		 //Mark a copy to keep the the original safe
		 return new Pair(p.getX(),p.getY());
	 }
	 
	 //Assume this is a time consuming operation
	 protected void store(Pair p){
		 storage.add(p);
			try {
				TimeUnit.MILLISECONDS.sleep(50);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
	 }
	 
	 public abstract void increment();
	 
 }
 
 //Synchronize the entire method
 class PairManager1 extends PairManager{

	@Override
	public synchronized void increment() {
		p.incrementX();
		p.incrementY();
		store(getPair());
	}
 }

 //Use a critical section 
 class PairManager2 extends PairManager{
	@Override
	public void increment() {
		Pair temp;
		synchronized (this) {
			p.incrementX();
			p.incrementY();
			temp = getPair();
		}
		store(temp);
	}
 }
 
 class PairManipulator implements Runnable{

	 private PairManager pm;
	 public PairManipulator(PairManager pm){
		 this.pm=pm;
	 }
	 
	@Override
	public void run() {
		while(true){
			pm.increment();
		}
	}
	
	@Override
	public String toString() {
		return "Pair: " + pm.getPair()+" checkCounter = " +pm.checkCount.get();
	}
 }
 
 class PairChecker implements Runnable{

	 private PairManager pm;
	 
	 public PairChecker(PairManager pm){
		 this.pm=pm;
	 }
	 
	@Override
	public void run() {
		while(true){
				pm.checkCount.incrementAndGet();
				pm.getPair().checkState();
		}
	}
 }
 
 
  
public class CriticalSection {
	
	static void testApproacher(PairManager pman1,PairManager pman2){
		ExecutorService exec = Executors.newCachedThreadPool();
		PairManipulator
			pm1 = new PairManipulator(pman1),
			pm2= new PairManipulator(pman2);
		PairChecker 
			pChecker1 = new PairChecker(pman1),
			pChecker2 = new PairChecker(pman2);
		exec.execute(pm1);
		exec.execute(pm2);
		exec.execute(pChecker1);
		exec.execute(pChecker2);
		try {
			TimeUnit.MILLISECONDS.sleep(500);
		} catch (InterruptedException e) {
			System.out.println("Sleep interrupted");
		}
		System.out.println("pm1:" +pm1 +"\npm2" +pm2);
		System.exit(0);
	}
	
	public static void main(String[] args) {
		PairManager pman1 = new PairManager1();
		PairManager pman2 = new PairManager2();
		testApproacher(pman1, pman2);
	}
}

class ExplictitPairManager1 extends PairManager{

	@Override
	public synchronized void increment() {
		Lock lock = new ReentrantLock();
		lock.lock();
		try{
			p.incrementX();
			p.incrementY();
			store(getPair());
		}finally{
			lock.unlock();
		}
	}
}

class ExplictitPairManager2 extends PairManager{

	@Override
	public synchronized void increment() {
		Pair temp;
		Lock lock = new ReentrantLock();
		lock.lock();	
		try{
			p.incrementX();
			p.incrementY();
			temp = getPair();
		}finally{
			lock.unlock();
		}
		store(temp);
	}
}


